iT邦幫忙

2022 iThome 鐵人賽

DAY 25
1
Modern Web

開始搞懂React生態系系列 第 25

Day 25 更有效率撰寫 Redux - Redux Toolkit

  • 分享至 

  • xImage
  •  

說明

一進 Redux Toolkit 的官網,就會看到以下示意圖。

它最初是為了解決使用 Redux 的三個常見問題

  • 配置 Redux Store 過於複雜
  • 要做 Redux 的任何事情,必須要自己手動添加相關的 Library
  • Redux 需要太多的樣板代碼

會希望若是能像 create-react-app 一樣,有一些開箱即用的工具、Library 及樣板代碼,就能夠讓開發者更專注於開發核心邏輯。

在這之後的文章將簡稱 Redux Toolkit 為 RTK。

安裝 RTK

使用 Create-Reate-App

直接把 --template 指定為 redux,就會內含有 Redux Toolkit

npx create-react-app my-app --template redux

在現有的應用加上 RTK

# NPM
npm install @reduxjs/toolkit 

# Yarn
yarn add @reduxjs/toolkit 

RTK 包含的 API

configureStore()

功用和 createStore 一樣可以建立 Store,結合 reducers、middleware。

createStore 是傳入參數的順序來加入 reducer 及 middleware。

configureStore 則是用 Options 形式來設定,程式碼看起來會更清楚。

若沒有指定 middleware,RTK 預設使用的是 redux-thunk。

-import { createStore } from "redux";
+import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './reducers';

- const store = createStore(rootReducer);
+ const store = configureStore({ reducer: rootReducer });

createAction()

建立 action creator 的函式。放在 createAction() 裡面的參數會自動變成 action type 字串常數。

import { createAction } from '@reduxjs/toolkit';

const fetchTodos = createAction('todos/fetchTodos');
// { type: 'todos/fetchTodos' }

const setFilter = createAction('filter/setFilter');
// setFilter('All')
// returns { type: 'filter/setFilter', payload: 'All' }

createReducer()

使用它在撰寫 reducer 的時候可以不用再用 switch case 語法,此外,它會自動使用 immerjs 讓您更簡單的處理狀態更新,例如 state.todos[3].completed = true。 (沒有使用 immutable 相關套件時,這樣的寫法會有 side-effect)。

import { createAction, createReducer } from '@reduxjs/toolkit';

const setFilter = createAction('filter/setFilter');

const initialState = 'All';

const filterReducer = createReducer(initialState, (builder) => {
  builder
    .addCase(setFilter, (state, action) => {
      state = action.payload;
    });
})

createSlice()

這個函式是 RTK 能有效率的開發 Redux 的重點,詳細說明如下。

createSlice 將 slice name、initial state、reducer、action 集中建立,在slice 檔案中。

createSlice 內部整合了 createReducer 和 createAction,因此 在大部分應用中, 不需特別寫這二個函式,只要使用 createSlice 就足夠。

import { createSlice } from '@reduxjs/toolkit';

const initialState = 'All';

const filterSlice = createSlice({
  name: 'filter',
  initialState,
  reducers: {
    setFilter(state, action) {
      state = action.payload
    },
  },
});

export const { setFilter } = filterSlice.actions;
export default filterSlice.reducer;

createSlice 函式的 Options 參數

createSlice() 接收一個 Options 作為參數

createSlice({
  name: ...
  initialState: ...
  reducers: {
    // 一般 reducer 定義的地方
    setFilter(state, action) {
      state = action.payload
    },
  },
  extraReducers: (builder) => {
    // 加入額外 reducer 的地方,使用 createAction 或 createAsyncThunk 建立的 action creators 都會設定在此  
  },
});
  • name:字串,被用於生成的 action type 的前綴 (eg. filter/setFilter)
  • initialState:reducer 的初始狀態值
  • reducers:Object,Key 會成為 action type string,函數就是對應的 reducers function。

createSlice 生成的 reducer 也會被稱為case reducers,它對應於之前寫法的 switch case,透過 createSlice 制作 reducer 後,就不需要再做 switch case。這裡沒有 default 處理函數。createSlice 生成的 reducer 會自動返回當前的 state。

createSlice 不用特別處理 mutable

沒有使用 RTK 以前,都要避免變動到當前的 state,你必須小心的處理及回傳新的 state 物件,或是套用 immutable 相關的套件。

createSlice 預設使用 Immer 套件處理,它把函數用 Immer 裡的 produce 封裝了起來。這意味著你可以寫任何修改 reducer 裡面的狀態的代碼,而 Immer 會安全地返回一個被正確地更新過的結果。

如果想要更深入理解原理,可以看看 RTK 官方的這篇說明,之後也會做為鐵人賽的補充,試著翻譯看看。

導出 action creators 及 reducer

slice 使用 createSlice 建立完成後,createSlice 會自動生成 action creators 及 reducer,就可以直接使用或是導出。

export const { setFilter } = filterSlice.actions;
export default filterSlice.reducer;

固定使用 action payload

使用 RTK 創造出來 action creators 只接受一個名稱為 palyload 的參數。如果 payload 是單一值,直接使用該值做為 payload 的全部。如果 payload 有多個值,payload 就是一個含有所有值的一個 Object。

createAsyncThunk

用以處理非同步,接受一個 action type string 和一個返回 promise 的函數,並生成一個發起基於該 promise 的 pending/fulfilled/rejected action 類型的 thunk。

後面實作串接 API 時,會再深入介紹,現在先知道 RTK 有 createAsyncThunk 用以處理非同步。

Redux Toolkit 實作範例

改寫 前面的 Redux 範例,Fork 出來一份,把 Todo MVC 改成使用 Redux Toolkit。

前置作業

  • 在 CodeSandBox 加上 @reduxjs/toolkit 相依

調整 store 改用 RTK 的 configureStore

import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "./reducers";

const store = configureStore({ reducer: rootReducer });
export default store;

建立 slice,導出 action creators 及 reducer

  • 因為封裝了 Immer,可以發現撰寫 reducers 內的 newState 處理邏輯更加簡單了。
  • 使用 RTK 創造出來 action creators 只接受一個 名稱為 palyload 的參數。
  • 寫完 slice 就等於把 action creators、reducer 都完成了,只要 export 就可以使用。日後專案修改邏輯時,可以不用頻繁的在檔案之間切換,只需要專注於 slice 的調整。

新增 todosSlice 檔案

這邊因為後面想加上 loading 的效果,所以 state 改用物件形式。

// store/slices/todosSlice.js

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  data: [],
  isLoading: false,
  error: false
};

const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    addTodo(state, action) {
      const { id, text } = action.payload;
      state.data.push({ id, text, completed: false });
    },
    toggleTodo(state, action) {
      const id = action.payload;
      const todo = state.data.find((todo) => todo.id === id);
      if (todo) {
        todo.completed = !todo.completed;
      }
    },
    deleteTodo(state, action) {
      const id = action.payload;
      state.data = state.data.filter((todo) => {
        return todo.id !== id;
      });
    }
  }
});

export const { addTodo, toggleTodo, deleteTodo } = todosSlice.actions;
export default todosSlice.reducer;

新增 filterSlice 檔案

// store/slices/filterSlice.js

import { createSlice } from "@reduxjs/toolkit";

const initialState = "All";

const filterSlice = createSlice({
  name: "filter",
  initialState,
  reducers: {
    setFilter(state, action) {
      state = action.payload;
      return state;
    }
  }
});

export const { setFilter } = filterSlice.actions;
export default filterSlice.reducer;

Tips:如果 payload 是單值的話,state 若直接指為 payload 記得 reuders function 一定回傳 state,否則會出現錯誤訊息如下圖。

組合 slice reducers 為 rootReducer

// reducers/index.js

import { combineReducers } from "redux";
import todos from "./slices/todosSlice";
import filter from "./slices/filterSlice";

const reducers = combineReducers({
  todos,
  filter,
});

export default reducers;

移除因為使用 createSlice 而不需要的檔案目錄

受益於 createSlice,將 slice name、initial state、reducer、action 集中建立在一個檔案,action 及 reducer 目錄都可以移掉。

在這邊我們也先移除 middleware 目錄,後面會再介紹 RTK 的用法。

在元件中使用

調整 action creators 從 slice 取得

變更 import { your-action-creators} from slices/xxxSlice

  • Footer
- import { setFilter, fetchTodosAsync } from "../store/actions";
+ import { setFilter } from "../store/slices/filterSlice";
// 為了加上 loading 小小調整一下 useSelector
- const todos = useSelector((state) => state.todos);
+ const todos = useSelector((state) => state.todos.data);
...
// 暫時註解 dispatch(fetchTodos());
+ onClick={() => {
+   // dispatch(fetchTodos());
+ }}
  • Header
- import { addTodo } from "../store/actions";
+ import { addTodo } from "../store/slices/todosSlice";

// 這裡小小調整一下,原本 id 自動產生邏輯是寫在 action creators 裡
- dispatch(addTodo(inputRef.current.value));
+ dispatch(
+   addTodo({
+     id: new Date().getTime().toString(),
+     text: inputRef.current.value
+   })
+);
  • TodoList
- import { toggleTodo, deleteTodo } from "../store/actions";
+ import { toggleTodo, deleteTodo } from "../store/slices/todosSlice";

// 為了加上 loading 小小調整一下 useSelector
- const todos = useSelector((state) => state.todos);
+ const todos = useSelector((state) => state.todos.data);

完整程式碼

完整程式碼操作:https://codesandbox.io/s/react-todomvc-redux-toolkit-nk0nsh

使用 Redux Toolkit 實作串接 API

前置作業

// api/todos.js
import axios from "axios";

const fetchTodos = () => {
  return axios.get("https://jsonplaceholder.typicode.com/users/1/todos");
};

export default { fetchTodos };
// api/index.js
import todosAPI from "./todos";
export { todosAPI };
  • 在元件加上 Loading 模版
// components/TodoList.js
...
const isLoading = useSelector((state) => state.todos.isLoading);
...
// 加上有 Loading 區塊的元件
const TodoListWithLoading = () => {
  if (isLoading) {
    return (
      <div style={{ padding: "10px", fontSize: "20px", textAlign: "center" }}>
        Loading...
      </div>
    );
  }
  return (
    <ul className="todo-list">
      {filteredTodos.map((todo) => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggleItem={() => dispatch(toggleTodo(todo.id))}
          onDeleteItem={() => dispatch(deleteTodo(todo.id))}
        />
      ))}
    </ul>
  );
};
...
// jsx 使用 <TodoListWithLoading />
return (
  <>
    <section className="main">
      <TodoListWithLoading />
    </section>
  </>
);

使用 createAsyncThunk 製作非同步 action creator

createAsyncThunk 接受二個參數

  • 一個產生 action types 的 string (會帶有 slice name 做為前綴)
  • 一個回傳 promise 的 callback function
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import { todosAPI } from "../../api";

// 這邊要導出,因為它不會納至 todoSlice.actions 中
export const fetchTodos = createAsyncThunk("todos/fetchTodos", async () => {
  const response = await todosAPI.fetchTodos();
  return response;
});

使用 createAsyncThunk 建立的非同步 action creators,就會自動產生下面的對應

// promise pending 等待中
fetchTodos.pending(); // action type => 'todos/fetchTodos/pending'
// promise fulfilled 正確完成
fetchTodos.fulfilled(); // action type => 'todos/fetchTodos/fulfilled'
// promise reject 已拒絕,操作失敗
fetchTodos.rejected(); // action type => 'todos/fetchTodos/rejected'

把 thunk action creators,加到 slice 檔案的 extraReducers 裡

const todosSlice = createSlice({
  name: "todos",
  initialState,
  reducers: {
    // 一般 reducer fuction 定義的地方
    ...
  },
  // 加入額外 reducer 的地方
  // 使用 createAction 或 createAsyncThunk 建立的 action creators 都會設定在此
  extraReducers: (builder) => {
    builder
      .addCase(fetchTodos.pending, (state, action) => {
        state.isLoading = true;
      })
      .addCase(fetchTodos.fulfilled, (state, action) => {
        state.data = action.payload.data.map(({ id, title, completed }) => {
          return {
            id,
            text: title,
            completed
          };
        });
        state.isLoading = false;
      });
  }

在元件中使用

import { fetchTodos } from "../store/slices/todosSlice";
...
// 把之前的註解取消掉
<span
  style={{ zIndex: 10 }}
  onClick={() => {
    dispatch(fetchTodos());
  }}
>
  Load Online Todos
</span>

完整程式碼

完整程式碼操作:https://codesandbox.io/s/react-todomvc-redux-toolkit-api-b4ob97

RTK Query

RTK 官方提供更簡單的方式去操作 API,可以先看看官方的 RTK Query 說明,之後也會做為鐵人賽的補充。

Next

Redux Toolkit (RTK) 幫我們簡化了使用 Redux 時的複雜操作步驟,接下來要介紹的 zustand 則是可以取代/加強 Redux 的狀態管理套件。它是可擴張式的狀態管理解決方案,讓你透過 hook 的方式直覺的採用狀態管理。

Reference

https://ithelp.ithome.com.tw/articles/10275089

https://github.com/DeerTeam/redux-toolkit-in-chinese

https://redux-toolkit-cn.netlify.app/

https://pjchender.dev/react/redux-toolkit/


上一篇
Day 24 Redux 非同步 Action 解決方案 - redux-observable
下一篇
Day 26 zustand - 基於 Flux 與 Hook實現狀態管理的套件
系列文
開始搞懂React生態系30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
janlin002
iT邦好手 1 級 ‧ 2023-03-03 20:51:56

寫得非常清楚,已收藏/images/emoticon/emoticon12.gif

我要留言

立即登入留言